#include <websocketpp/config/asio_client.hpp>
#include <websocketpp/client.hpp>
#include <iostream>
#include <string>
#include <thread>
#include <unordered_map>
#include <curl/curl.h>
#include <openssl/hmac.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/buffer.h>
#include <nlohmann/json.hpp>
#include "fmtlog.h"

using client = websocketpp::client<websocketpp::config::asio_tls_client>;
using context_ptr = std::shared_ptr<boost::asio::ssl::context>;
using websocketpp::lib::bind;
using websocketpp::lib::placeholders::_1;
using message_ptr = websocketpp::config::asio_client::message_type::ptr;

std::string formatNumber(std::string number, int n) {
    std::string::size_type pos = number.find('.');
    if (pos != std::string::npos) {
        int decimalLength = number.size() - pos - 1;
        if (decimalLength >= n) {
            return number.substr(0, pos + 1 + n);
        }
    } 
    return number;
}

std::string UnixTimestamp_mills() {  
    auto now = std::chrono::system_clock::now();  
    auto time_since_epoch = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();  
    return std::to_string(time_since_epoch);  
} 

class BinanceArbitrage {
public:
    BinanceArbitrage() {
        m_data_client.init_asio();
        m_data_client.set_tls_init_handler([](websocketpp::connection_hdl) {
            return websocketpp::lib::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tlsv12);
        });
        m_data_client.set_message_handler(std::bind(&BinanceArbitrage::on_data_message, this, std::placeholders::_1, std::placeholders::_2));
        m_data_client.set_open_handler(std::bind(&BinanceArbitrage::on_data_open, this, std::placeholders::_1));
        m_data_client.set_close_handler(std::bind(&BinanceArbitrage::on_data_close, this, std::placeholders::_1));

        m_trade_client.init_asio();
        m_trade_client.set_tls_init_handler([](websocketpp::connection_hdl) {
            return websocketpp::lib::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tlsv12);
        });
        m_trade_client.set_message_handler(std::bind(&BinanceArbitrage::on_trade_message, this, std::placeholders::_1, std::placeholders::_2));
        m_trade_client.set_open_handler(std::bind(&BinanceArbitrage::on_trade_open, this, std::placeholders::_1));
        m_trade_client.set_close_handler(std::bind(&BinanceArbitrage::on_trade_close, this, std::placeholders::_1));

        price_adausdt = -1;
        price_usdcusdt = -1;
        price_adausdc = -1;
        forward = 0;
        init_usdt = 100;
    }

    void connect_data_ws(const std::string& uri) {
        while(true){
            websocketpp::lib::error_code ec;
            client::connection_ptr con = m_data_client.get_connection(uri, ec);
            if (ec) {
                std::cout << "Could not create data connection because: " << ec.message() << std::endl;
                return;
            }
            m_data_client.connect(con);
            std::thread data_ws(&BinanceArbitrage::run_data_client,this);
            data_ws.join();
            m_data_client.reset();
        }
    }

    void connect_trade_ws(const std::string& uri) {
        userStreamKey = ListenKey(false);
        std::thread keep_alive(&BinanceArbitrage::keep_user_stream,this);
        while(true){
            websocketpp::lib::error_code ec;
            
            client::connection_ptr con = m_trade_client.get_connection(uri + userStreamKey, ec);
            if (ec) {
                std::cout << "Could not create trade connection because: " << ec.message() << std::endl;
                return;
            }
            m_trade_client.connect(con);
            std::thread trade_ws(&BinanceArbitrage::run_trade_client,this); 
            trade_ws.join();
            m_trade_client.reset();
        }
        keep_alive.join();
    }

    void keep_user_stream(){
        int keepAliveCount = 0;
        while(true){
            std::this_thread::sleep_for(std::chrono::seconds(60));
            keepAliveCount += 1;
            if(keepAliveCount > 50){
                userStreamKey = ListenKey(true);
                keepAliveCount = 0;
            }
        }
    }

    virtual void on_trade_message(websocketpp::connection_hdl hdl, message_ptr msg) {
    }

    virtual void on_data_message(websocketpp::connection_hdl hdl, message_ptr msg) {
    }

    void run_data_client(){
        m_data_client.run();
    }

    void run_trade_client(){
        m_trade_client.run();
    }

    void on_data_open(websocketpp::connection_hdl hdl) {
        std::cout << "Connected to Market Data WebSocket" << std::endl;
    }

    void on_data_close(websocketpp::connection_hdl hdl) {
        std::cout << "Disconnected from Market Data WebSocket" << std::endl;
    }

    void on_trade_open(websocketpp::connection_hdl hdl) {
        std::cout << "Connected to Trade WebSocket" << std::endl;
    }

    void on_trade_close(websocketpp::connection_hdl hdl) {
        std::cout << "Disconnected from Trade WebSocket" << std::endl;
    }

    std::string getSinnature(const std::string& data){
        unsigned char* result;
        static char res_hexstring[65];
        result = HMAC(EVP_sha256(), secretKey.c_str(), secretKey.length(), (unsigned char*)data.c_str(), data.length(), NULL, NULL);
        for (int i = 0; i < 32; i++) {
            sprintf(&(res_hexstring[i * 2]), "%02x", result[i]);
        }
        return std::string(res_hexstring, 64);
    }

    static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
        ((std::string*)userp)->append((char*)contents, size * nmemb);
        return size * nmemb;
    }

    std::string ListenKey(bool isPut){
        std::string listenKey;

        CURL* curl;
        CURLcode res;
        std::string readBuffer;
        curl = curl_easy_init();
        if (!curl) {
            std::cerr << "Failed to initialize CURL" << std::endl;
            return listenKey;
        }
        struct curl_slist* headers = NULL;
        headers = curl_slist_append(headers, ("X-MBX-APIKEY: " + apiKey).c_str());
        curl_easy_setopt(curl, CURLOPT_URL, "https://api.binance.com/api/v3/userDataStream");
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
        if(isPut){
            curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
        }
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "");
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
        res = curl_easy_perform(curl);
        curl_slist_free_all(headers);
        curl_easy_cleanup(curl); 
        if (res != CURLE_OK) {
            std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << std::endl;
        } else {
            try {
                nlohmann::json jsonData = nlohmann::json::parse(readBuffer);
                if (jsonData.contains("listenKey")) {
                    listenKey = jsonData["listenKey"];
                }
            } catch (const nlohmann::json::parse_error& e) {
                std::cerr << "JSON parsing error: " << e.what() << std::endl;
            }
        }
        return listenKey;
    }

public:
    std::string apiKey;
    std::string secretKey;
    std::string userStreamKey;
    client m_data_client;   
    client m_trade_client;
    double price_adausdt;
    double price_usdcusdt;
    double price_adausdc;
    double init_usdt;
    int count;
    double volumn;
    int forward;   

    std::string getSpotQuantity(const std::string& symbol) {
        std::string quantity ="0";
        CURL* curl;
        CURLcode res;
        std::string readBuffer;

        curl = curl_easy_init();
        if(curl) {
            std::string query_string = "timestamp=" + UnixTimestamp_mills();
            std::string url = "https://api.binance.com/api/v3/account?" + query_string + "&signature=" + getSinnature(query_string);
            struct curl_slist* headers = NULL;
            headers = curl_slist_append(headers, ("X-MBX-APIKEY: " + apiKey).c_str());
            curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
            curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
            curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
            curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
            res = curl_easy_perform(curl);
            curl_easy_cleanup(curl);
            if(res != CURLE_OK) {
                std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << std::endl;
            }else{
                //std::cout<<readBuffer<<std::endl;
                nlohmann::json account = nlohmann::json::parse(readBuffer);
                for(const auto& coins : account["balances"]){
                    if(coins["asset"] == symbol){
                        quantity = coins["free"];
                        return quantity;
                    }
                }
            }  
        }
        return "0";
    }

    void place_order(const std::string& symbol, const std::string& side, const std::string& quantity, const std::string& price,const std::string& q_type) {
        CURL* curl;
        CURLcode res;
        std::string readBuffer;

        curl = curl_easy_init();
        if (curl) {
            std::string post_data = "symbol=" + symbol + "&side=" + side + "&type=MARKET&"+ q_type + "=" + quantity + "&price=" + price + "&timeInForce=GTC&recvWindow=5000&timestamp=" + UnixTimestamp_mills();
            //std::cout<<post_data<<std::endl;
            std::string payload = post_data + "&signature=" + getSinnature(post_data);
            struct curl_slist* headers = NULL;
            headers = curl_slist_append(headers, ("X-MBX-APIKEY: " + apiKey).c_str());
            curl_easy_setopt(curl, CURLOPT_URL, "https://api.binance.com/api/v3/order");
            curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload.c_str());
            curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
            curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);
            res = curl_easy_perform(curl);
            curl_slist_free_all(headers);
            curl_easy_cleanup(curl);
            if (res != CURLE_OK){
                fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
            }else{
                std::cout<<"send_order: "<<readBuffer<<std::endl;
            }
        }
    }
};